Operaciones múltiples e iteración

Encuentro 4

Operaciones simultáneas por columnas


La filosofía de trabajo de tidyverse se plantea nunca copiar y pegar más de dos veces el código escrito, pero cuando necesitamos realizar la misma operación en un conjunto de variables simultáneamente nos encontramos con este problema.

La solución, ofrecida dentro de dplyr, es un andamiaje que permite aplicar funciones y expresiones a varias columnas simultáneamente.

Es una forma de iteración, donde se repite la misma acción en diferentes objetos. En este caso los objetos serán columnas (variables) de la tabla de datos.


Las operaciones simultáneas pueden darse como transformación (dentro de un mutate()) o de resumen (dentro de un summarise())

Operaciones simultáneas por columnas


Creación de múltiples columnas con mutate()


Resumiendo múltiples columnas con summarise()

Función across()


La función across() es la encargada de dar soporte a estas operaciones múltiples (dplyr >= 1.0.0).


across(.cols,  .fns,  ...,  .names)


.cols = columnas a transformar

.fns = función o funciones para aplicar a cada columna de .cols

... = argumentos adicionales de las funciones especificadas anteriormente (ejemplo: na.rm = T)

.names = nombres de las columnas de salida. Aquí, {.col} es un marcador especial al que se le puede agregar el sufijo deseado.

Resúmenes múltiples


Tomemos la siguiente tabla de datos ficticios (mostramos las primeras 4 observaciones):

# A tibble: 4 × 4
        a     b      c      d
    <dbl> <dbl>  <dbl>  <dbl>
1 -0.560  1.22  -1.07   0.426
2 -0.230  0.360 -0.218 -0.295
3  1.56   0.401 -1.03   0.895
4  0.0705 0.111 -0.729  0.878


Supongamos que queremos calcular la media de cada variable…

Resúmenes múltiples

Podríamos hacerlo repitiendo para cada variable

datos |> summarise(
  a = mean(a),
  b = mean(b),
  c = mean(c),
  d = mean(d),
)
# A tibble: 1 × 4
       a     b      c     d
   <dbl> <dbl>  <dbl> <dbl>
1 0.0746 0.209 -0.425 0.322


Pero esto rompe la regla general que buscamos de nunca copiar y pegar más de dos veces…

Resúmenes múltiples


Para solucionarlo aplicamos across() y realizamos el resumen simultáneo en una sola línea.

datos |> summarise(
  across(.cols = a:d, .fns = mean),
)
# A tibble: 1 × 4
       a     b      c     d
   <dbl> <dbl>  <dbl> <dbl>
1 0.0746 0.209 -0.425 0.322


Nótese que el primer argumento es el rango de nombres de variables y el segundo la función que aplicamos a todas ellas (nombres de funciones sin paréntesis).

Seleccionar variables (.cols)


El primer argumento de across() responde de la misma forma que la función select() y aplican también las funciones ayudantes de selección.

names(datos)
[1] "grupo" "a"     "b"     "c"     "d"    
datos |> 
  group_by(grupo) |> 
  summarize(across(everything(), mean))
# A tibble: 2 × 5
  grupo      a       b      c      d
  <int>  <dbl>   <dbl>  <dbl>  <dbl>
1     1 -0.191 -0.331  -0.485 -0.357
2     2  0.132 -0.0552  0.421  0.239

Recordamos a las funciones ayudantes de selección

  • everything(): coincide con todas las variables.

  • group_cols(): seleccione todas las columnas de agrupación.

  • starts_with(): comienza con un prefijo.

  • ends_with(): termina con un sufijo.

  • contains(): contiene una cadena literal.

  • matches(): coincide con una expresión regular.

  • num_range(): coincide con un rango numérico como x01, x02, x03.

  • all_of(): coincide con nombres de variables en un vector de caracteres. Todos los nombres deben estar presentes; de lo contrario, se generará un error de fuera de límites.

  • any_of(): igual que all_of(), excepto que no se genera ningún error para los nombres que no existen.

  • where(): aplica una función a todas las variables y selecciona aquellas para las cuales la función regresa TRUE.

Expresiones de selección


El argumento .cols también puede recibir construcciones booleanas utilizando los operadores conocidos como ! (negación) y conectores lógicos como & (AND) y | (OR) entre las funciones ayudantes de selección.


Por ejemplo:

.cols = !where(is.numeric) & starts_with("a")


Selecciona todas las columnas no numéricas, cuyo nombre comienza con “a”.

Agregar argumentos a las funciones


Hasta ahora vimos el ejemplo de aplicar una función simple como mean() a un grupo de variables.


Que sucede si entre los datos de esas variables hay valores NA?

Vamos a necesitar incorporar el argumento na.rm = TRUE a la función.


Donde lo hacemos dentro de un across()?

Agregar argumentos a las funciones


Supongamos que tenemos estos datos (mostramos algunas observaciones):

# A tibble: 4 × 4
       a      b      c      d
   <dbl>  <dbl>  <dbl>  <dbl>
1  1.56  -1.27  NA     -0.473
2 -0.560 NA     -1.05  -1.07 
3 -0.230  1.22   0.238 -0.218
4 NA     -0.446  1.29  -1.03 


Vemos algunos valores NA entre las observaciones.

Agregar argumentos a las funciones


Si aplicamos el mismo código de across() anterior tendríamos como resultado:

datos_na |> 
  summarise(
    across(a:d, mean)
  )
# A tibble: 1 × 4
      a     b     c      d
  <dbl> <dbl> <dbl>  <dbl>
1    NA    NA    NA -0.703


Sería bueno que le pasaramos na.rm = TRUE a la función mean().

Agregar argumentos a las funciones


Existen dos formas sintácticas de hacerlo.

  • Una función estilo-purrr (tidyverse): ~ mean(.x, na.rm = TRUE)

  • Una función anónima (base): function(x) mean(x, na.rm = TRUE) ; o mejor en su forma de atajo: \(x) mean(x, na.rm = TRUE)

datos_na |> 
  summarise(
    across(a:d, \(x) mean(x, na.rm = TRUE))
  )
# A tibble: 1 × 4
      a      b     c      d
  <dbl>  <dbl> <dbl>  <dbl>
1 0.210 -0.293 0.161 -0.703

Múltiples funciones


Para incorporar más de una función dentro de across() debemos incluirlas dentro de una lista [list()]

datos_na |> 
  summarise(
    across(a:d, list(
      media = \(x) mean(x, na.rm = TRUE),
      n_na = \(x) sum(is.na(x))))
  )
# A tibble: 1 × 8
  a_media a_n_na b_media b_n_na c_media c_n_na d_media d_n_na
    <dbl>  <int>   <dbl>  <int>   <dbl>  <int>   <dbl>  <int>
1   0.210      1  -0.293      1   0.161      2  -0.703      0

La lista contiene cada función a aplicar, bajo nombres definidos.

Cambiar nombres de resultados


Observemos que los nombres de las variables resultado se componen del nombre de la columna, un guión bajo y el nombre definido de la función aplicada, para distinguir entre las múltiples funciones del across().

La estructura de estos nombres se pueden modificar con el argumento .names de across().

Los marcadores especiales para el nombre de columna es {.col} y para el nombre de la función definida es {.fn}.

Cambiar nombres de resultados


Por ejemplo, podríamos invertir el orden predeterminado de los nombres del resumen.

datos_na |> 
  summarise(
    across(a:d, list(
      media = \(x) mean(x, na.rm = TRUE),
      n_na = \(x) sum(is.na(x))),
      .names = "{.fn}_{.col}")
  )
# A tibble: 1 × 8
  media_a n_na_a media_b n_na_b media_c n_na_c media_d n_na_d
    <dbl>  <int>   <dbl>  <int>   <dbl>  <int>   <dbl>  <int>
1   0.210      1  -0.293      1   0.161      2  -0.703      0

Transformación de tipos de datos


Hasta ahora vimos como funciona la función across() dentro de un resumen (summarise) pero al comienzo también dijimos que se puede utilizar para transformaciones masivas de datos.


Para lograr esto la función se vincula con mutate() modificando las variables originales o bien creando nuevas variables si cambiamos su nombre con .names.

Transformación de tipos de datos


Aplicamos la función coalesce() para convertir los valores NA en ceros, transformando las variables originales.

datos_na |> 
  mutate(
    across(a:d, \(x) coalesce(x, 0))
  )
# A tibble: 5 × 4
        a      b      c      d
    <dbl>  <dbl>  <dbl>  <dbl>
1  1.56   -1.27   0     -0.473
2 -0.560   0     -1.05  -1.07 
3 -0.230   1.22   0.238 -0.218
4  0      -0.446  1.29  -1.03 
5  0.0705 -0.687  0     -0.729

Transformación de tipos de datos


Hacemos lo mismo pero cambiamos los nombres de las variables de salida del mutate() que van a coexistir con las originales.

datos_na |> 
  mutate(
    across(a:d, \(x) coalesce(x, 0),
      .names = "{.col}_na_cero")
  )
# A tibble: 5 × 8
        a      b      c      d a_na_cero b_na_cero c_na_cero d_na_cero
    <dbl>  <dbl>  <dbl>  <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
1  1.56   -1.27  NA     -0.473    1.56      -1.27      0        -0.473
2 -0.560  NA     -1.05  -1.07    -0.560      0        -1.05     -1.07 
3 -0.230   1.22   0.238 -0.218   -0.230      1.22      0.238    -0.218
4 NA      -0.446  1.29  -1.03     0         -0.446     1.29     -1.03 
5  0.0705 -0.687 NA     -0.729    0.0705    -0.687     0        -0.729

Filtros


En el caso de iteraciones similares para incluir dentro de la función filter() el paquete dplyr propone dos funciones específicas: if_any() e if_all().


En el primer caso, la función enmascara una repetición de OR lógicos y en la segunda una secuencia de AND lógicos.

Filtros

datos_na |> filter(if_any(a:d, is.na))
# A tibble: 4 × 4
        a      b     c      d
    <dbl>  <dbl> <dbl>  <dbl>
1  1.56   -1.27  NA    -0.473
2 -0.560  NA     -1.05 -1.07 
3 NA      -0.446  1.29 -1.03 
4  0.0705 -0.687 NA    -0.729

Es lo mismo que filter(is.na(a) | is.na(b) | is.na(c) | is.na(d))


datos_na |> filter(if_all(a:d, is.na))
# A tibble: 0 × 4
# ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>

Es lo mismo que filter(is.na(a) & is.na(b) & is.na(c) & is.na(d))

Filtros


Las dos funciones de filtro trabajan con el mismo esquema que across(), por lo tanto se le puede aplicar una función o expresión de condición (debe devolver TRUE o FALSE)


datos  |> filter(if_all(a:d, \(x) x > -0.5 & x < 1))
# A tibble: 2 × 5
  grupo     a     b      c       d
  <int> <dbl> <dbl>  <dbl>   <dbl>
1     2 0.780 0.124  0.922 0.181  
2     2 0.253 0.380 -0.491 0.00576

Estructuras de control de flujo


Bucles tradicionales

  • for(): secuencia de elementos

  • while(): mientras una condición es verdadera

  • repeat(): repetición y control manual con break (uso peligroso)


Mapeo con paquete purrr

  • Familia funciones map()

Función for()

Función for()


for (variable in vector) {
  
}


Donde variable se reemplaza por un índice (generalmente 1 pero puede llamarse como deseemos) que recorrerá un vector desde 1 a una determinada longitud. (habitualmente 1:length(x)).

En cada vuelta la variable aumenta 1 hasta que alcanza el final del vector.

La variable i se utiliza en el cuerpo del for() para recorrer índices de objetos.

Función while()

Función while()


while (condition) {
  
}


Donde condition es una condición lógica. Si se cumple el bucle continúa, de lo contrario se sale de él.

Dentro del cuerpo de la estructura debemos proceder a manejar los cambios en lo que se evalúa en la condición. Si eso no sucede, el bucle puede llegar a ser infinito.

El control es más “artesanal” que el bucle for() y depende completamente del usuario del lenguaje.

Paquete purrr


Paquete con herramientas que buscan remplazar las formas tradicionales de bucles iterativos otorgándole compatibilidad con tidy data (datos ordenados).

  • Se utiliza para aplicar funciones a vectores, dataframes y listas, dando lugar a la denominada “programación funcional” (FP).

  • El paquete se instala y activa con tidyverse.

  • Sus funciones son faciles de escribir pero más dificiles de entender para usuarios sin conocimientos de programación.

  • Las familia de funciones map() son similares en idea que la familia de funciones apply() de R base pero consistentes con el ecosistema tidyverse.

Familia map()